今天要分享的是 Intersection Observer API,同樣是 Web Observer 家族 XD,他是用來觀察 DOM 元素與可視區域 (viewport) 或另一個元素之間的相交情況 (intersection)。我們可以監看元素是否進入或退出了可視區域,從而觸發相應的行為。
網路上的範例都是以懶加載 (Lazy Load) 以及無限滾動 (infinite scroll) 居多,我們只要思考一下這兩個技術的實現邏輯,就大概能了解 Intersection Observer API 的監聽方式,核心在於監控元素是否可視區域 (viewport),並根據結果執行特定的操作。
跟 MutationObserver API 一樣,我們要先建立一個 Observer 物件,並設定監聽的元素
首先建立 IntersectionObserver 物件。該物件接受一個 callback 和一個可選的 options:
const observer = new IntersectionObserver(callback, options);
options 常見的屬性如下:
const options = {
  root: document.querySelector('.scroll-container'),
  rootMargin: '0px',
  threshold: 0.5
};
根元素 (Root)
root 選項用來設置觀察的根元素,如果不設置,默認為視窗可視區域 (viewport)。
const options = {
  root: document.querySelector('.scroll-container'),
  threshold: 0.5
};
const observer = new IntersectionObserver(callback, options);
根元素的邊界 (RootMargin)
rootMargin 可以調整 root 的大小。我們可以擴大或縮小根元素的邊界,從而改變被視為「可見」的區域。使用方式類似 CSS 的 margin 屬性,可以設定四個方向的值,用法與格式也都雷同,例如:rootMargin: '10px 20px 10px 20px',就是上下邊界為 10px,左右邊界為 20 px。
我們還能對 rootMargin 設定正負值,正值會擴大觀察區域,負值會縮小觀察區域。常見的情境如下:
閾值 (Threshold)
threshold 的意思是閾值,意思是我們要觸發某個變化時,需要滿足某個條件的值,這個值就是閾值 XXD,小時候理化可能有學過(?)。在此就是用來決定目標元素與 DOM 元素或可視區域 (viewport) 相交的比例達到什麼程度時,會觸發 callback。
threshold 的型態是 number,或者可以是一個陣列數字 number[],範圍從 0 到 1,代表目標元素的可見部分比例。
例如:
const options = {
  threshold: [0, 0.5, 1]
};
const observer = new IntersectionObserver(callback, options);
在這個例子中,當目標元素從完全不可見 (0) 到 50% 可見 (0.5),再到完全可見 (1)時,都會執行 callback。
接著使用 observe() 方法監聽目標元素:
const target = document.querySelector('.target-element');
observer.observe(target);
以上的程式碼表示,當 .target-element 與 .scroll-container  元素相交時,就會再看 threshold 的設定來決定是否觸發 callback
讓我們把它完整的寫一遍,再加上一些效果:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Intersection Observer Example</title>
  <style>
    /* 設置容器樣式以模擬滾動區域 */
    .scroll-container {
      width: 100%;
      height: 300px;
      overflow-y: scroll;
      border: 1px solid #ccc;
      position: relative;
    }
    /* 設置一個很高的高度來提供滾動空間 */
    .scroll-container::before {
      content: '';
      display: block;
      height: 500px;
    }
    .scroll-container::after {
      content: '';
      display: block;
      height: 500px;
    }
    /* 目標元素樣式 */
    .target-element {
      width: 100px;
      height: 100px;
      background-color: #3498db;
      margin: 20px auto;
      opacity: 0;
      transform: translateY(50px);
      transition: opacity 0.5s ease, transform 0.5s ease;
    }
    /* 當元素進入可視區域時添加的類 */
    .visible {
      opacity: 1;
      transform: translateY(0);
    }
  </style>
</head>
<body>
  
  <div class="scroll-container">
    <div class="target-element"></div>
  </div>
  
  <script>
    const options = {
      root: document.querySelector('.scroll-container'),
      threshold: 0.5
    };
    const observer = new IntersectionObserver(callback, options);
    const target = document.querySelector('.target-element');
    observer.observe(target);
    function callback(entries) {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          entry.target.classList.add('visible');
          console.log('目標元素在可視區域內');
        } else {
          entry.target.classList.remove('visible');
          console.log('目標元素在可視區域外');
        }
      });
    }
  </script>
</body>
</html>

https://mukiwu.github.io/web-api-demo/observer.html
透過上面的範例,可以清楚地看出 Intersection Observer API 非常適合做懶加載 (Lazy Load )和無限滾動 (Infinity Scroll) 功能。只需要設定好觀察目標和閾值,就能輕鬆實現所需的效果,無需像過去那樣手動處理座標計算。
以上有任何問題,都歡迎留言討論。